자바스크립트 Deep Dive 책을 읽던 중 아래 내용을 만났다.
'0' == '' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == null // false
false == undefined // false
책에서는 안티 패턴이므로 이해하려 하지 않아도 된다고 한다.
하지만 내가 이 책을 읽게 된 계기는 자바스크립트의 기초가 부족하다고 생각했고, 남에게 설명할 수 없다고 판단했기 때문이다.
따라서 왜 그런지 알고 넘어가야겠다.
우선 0 == ''
는 true
이고 0 == '0'
는 true
인데 왜 '0' == ''
는 false
일까?
내 생각에 이는 아마 숫자가 들어가는 경우 양 타입을 숫자로 변환하기 때문이다.
이 표현식은 0
이 포함되었으므로, ''
는 숫자로 변환된다. 이때 ''
는 0으로 변환되므로 0 == 0
으로 처리된다.
이 표현식 또한 0이 포함되었으므로, '0'
은 순자로 변환되고, 이는 0이므로 0 == 0
으로 처리된다.
이 표현식은 둘다 문자열이다. 타입이 같으므로 그 자체로 비교를 하며 '0'
과 ''
는 다르므로, false로 처리된다.
기억상 null과 undefined는 falsy한 값으로 알고 있다. 조건문의 조건으로 해당 값들이 들어가면 false로 처리되기 때문이다.
‘false’는 이미 과거에 how to convert string to boolean으로 검색해서 숫자와 다르게 불린은 캐스트가 불가능하다고 알고 있었다.
그런데 이들은 느슨한 동등 연산에서 false와 다르다고 나오고, ‘0’는 또 같다고 나온다. ‘0’는 falsy한 값이 아니다. 이는 왜일까?
우선 false
는 falsy값으로 확정이니, 어떻게 하면 '0'
이 falsy한 값이 될 수 있는가를 생각해본다.
'0'
자체로는 falsy할 수 없다. 다만 숫자 0은 falsy 한 값이다. 그럼 false == toNumber(‘0’) 으로 변환된다.
위 값은 false == 0이 되고 다시한번 false와 0의 형을 맞추기 위해 false == toBoolean(0)으로 변환한다.
그럼 false == false가 되어 true가 된다.
위에서 처럼 null을 false와 형을 맞추기 위해 false == toBoolean(null)으로 변환한다.
그럼 false == false가 되어 true가 된다. 근데 해당 값은 false가 정답이라 잘못된 접근이다.
false == null과 동일하게 처리하면 역시 true가 되어야 하는데, false가 정답이다.
불린 false와의 연산은 추측조차 실패했다. 문자열 ‘0’과 숫자 0의 연산은 그럴듯한 추측이 되었다.
하지만 나에게 중요한건 기초를 확실히 알고 있고, 남에게 설명할 수 있는가 이다.
그래서 ecma 스펙을 읽어봤다.
여기서는 5.1 기준으로 확인했다.
https://262.ecma-international.org/5.1/#sec-11.9.1
위 내용에 따르면 lval과 rval을 GetValue로 가져와서 rval == lval
에 대한 abstract equality comparison(11.9.3)를 따른다고 한다.
https://262.ecma-international.org/5.1/#sec-11.9.3
11.9.3의 1-d에 따르면 x와 y가 문자열로 타입이 같으면 x와 y가 완전히 동일한 문자의 연속을 가지고 있으면 true를 리턴한다고 되어있다.
이 때 완전히 동일한 문자의 연속은 아래를 만족한다.
따라서 '' == '0'
는 완전히 동일한 문자의 연속을 만족하지 않기 때문에 false이다.
11.9.3의 4와 5에 따르면 문자열과 숫자의 연산의 경우, 문자열에 toNumber를 처리하여 동등 연산을 하도록 되어있다.
이에 따라 0 == toNumber('0')
과 0 == toNumber('')
는 모두 0 == 0
이 된다.
11.9.3의 1-c-iii에 의하여 같은 숫자 값을 가진 경우 true를 리턴한다.
따라서 0 == '0'
과 0 == ''
은 0 == 0
으로 처리되며 같은 숫자를 가진 경우로 true이다.
11.9.3의 6과 7에 따르면 x와 y 중 불린이 있는 경우 불린에 toNumber를 처리하여 동등 연산을 하도록 되어있다.
이에 따라 위 식 세가지는 아래처럼 변경된다.
false == '0'
=> toNumber(false) == '0'
=> 0 == '0'
=>
toNumber(false) == null=>
0 == null`false == undefined
=> toNumber(false) == undefined
=> 0 == undefined
1번의 경우 위의 문자열과 숫자의 연산에서 이미 해결되었으므로 true가 된다.
2번과 3번의 경우 null
과 undefined
가 11.9.3의 1번부터 9번 사이에 매칭되는 경우가 없다
따라서 11.9.3의 10에 의해 false가 리턴된다.
그동안 깊이 생각 해본 적은 없지만, falsy한 값은 항상 false와의 느슨한 동등 연산에서 같은 값을 띄울거라 당연하게 생각했다.
그리고 false와의 느슨한 동등 연산이 true인 값은 항상 falsy할 것이라고 생각했다.
돌이켜 보니 이런 생각이 잘못된 코드를 작성하게 만들었던 경험이 있던 것 같은데 왜 깊게 생각하지 않았나 후회된다.
// ex) [] == false : true 라서 빈 배열이 falsy할 거라는 착각
const list = []
if(list) {
console.log('list는 빈 배열이 아닐 것이다') // 빈 배열일 경우에도
}
위 내용을 작성하며 그동안 내가 너무 짧게 생각했다는 것을 돌아보게 되었다.
이번 내용을 위해 처음으로 ecma 명세를 일부 직접 읽어봤는데 글자가 많아 아득하면서도 새로운 사실을 알아가는게 즐거운 것 같다.
오늘부터 꾸준히 이런것들을 알아보는 시간을 가져야겠다.
위에서는 5.1 기준의 스펙을 읽었다. 최신인 13을 읽고 싶었으나, 내용이 너무 방대해서 pdf 뷰어가 버벅였기 때문인데, 아래에 5.1과 13의 매칭 부분을 남긴다.
13버전 ecma 스펙 : https://262.ecma-international.org/13.0/index.html
11.9.1 The Equals Operator => 13.11.1 Runtime Semantics: Evaluation
11.9.3 The Abstract Equality Comparison Algorithm => 7.2.15 IsLooselyEqual